﻿using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Collections.Generic;
using Red.Toolkit.Profiler.Logic;

namespace Red.Toolkit.Profiler.Drawing
{
    public class TimelineComponent : FrameworkElement
    {
        public delegate void ShowSelectionDetailsDelegate( string msg );

        private enum SelectionUpdateMode
        {
            Set,
            Unset,
            Add,
            Invert
        };

        private enum MouseDragMode
        {
            None,
            Select,
            Move
        }

        private ShowSelectionDetailsDelegate _onShowSelectionDetails;

        private IList<ProfilerTraceInstance> _traces;
        private IList<Bookmark> _bookmarks;
        private TimelineStyle _timelineStyle;
        private TimelineParams _timelineParams;

        private MouseDragMode _mouseDragMode;
        private Point _mouseDragStart;
        private Point _mouseDragLast;
        private double _mouseDragStartTimeOffset;
        private double _mouseDragStartYOffset;
        private Point _mouseLastHoverPos;

        private bool _timeSelection;
        private Point _selectionRectStart;
        private Point _selectionRectEnd;

        private HashSet<Event> _selection;
        private SortedDictionary<Event, Rect> _drawnEvents;

        public TimelineComponent()
        {
            children = new VisualCollection( this );
            drawingVisual = new DrawingVisual();
            children.Add( drawingVisual );

            _onShowSelectionDetails = null;
            _timelineStyle = null;
            _timelineParams = null;
            _traces = null;
            _bookmarks = null;
            _selection = new HashSet<Event>();
            _drawnEvents = new SortedDictionary<Event, Rect>(new Event.Comparer());

            _mouseDragLast = new Point( 0, 0 );
            _mouseLastHoverPos = new Point( -1, -1 );
            _mouseDragMode = MouseDragMode.None;
            _mouseDragStartYOffset = 0;

            // set up event handlers
            Loaded += new RoutedEventHandler( OnLoaded );
            MouseDown += new MouseButtonEventHandler( OnMouseDown );
            MouseMove += new MouseEventHandler( OnMouseMove );
            MouseUp += new MouseButtonEventHandler( OnMouseUp );
            SizeChanged += new SizeChangedEventHandler( OnSizeChanged );
            LostFocus += new RoutedEventHandler( OnFocusLost );
            MouseWheel += new MouseWheelEventHandler( OnMouseWheel );
            KeyDown += new KeyEventHandler( OnKeyDown );
        }

        #region DrawingVisual stuff

        private VisualCollection children;
        private DrawingVisual drawingVisual;

        protected override int VisualChildrenCount
        {
            get { return children.Count; }
        }

        protected override Visual GetVisualChild( int index )
        {
            if ( index < 0 || index >= children.Count )
                throw new ArgumentOutOfRangeException();

            return children[index];
        }

        #endregion DrawingVisual stuff

        public void AdjustView()
        {
            double maxLen = 0.0;

            foreach ( var trace in _traces )
                foreach ( var view in trace.DataViews )
                    if ( view.Visible )
                        maxLen = Math.Max( maxLen, view.Length );

            _timelineParams.TimeRangeEnd = maxLen * 1.05;
            _timelineParams.TimeOffset = 0.0;
            _timelineParams.PixelsPerSecond = ActualWidth / (_timelineParams.TimeRangeEnd - _timelineParams.TimeRangeStart);

            _timelineParams.OnDrawDelegate();
        }

        public void SetOnShowSelectionDetailsDelegate( ShowSelectionDetailsDelegate d )
        {
            _onShowSelectionDetails = d;
        }

        public void SetBookmarksContainer( IList<Bookmark> bookmarks )
        {
            _bookmarks = bookmarks;
        }

        public void SetUp( IList<ProfilerTraceInstance> traces, TimelineStyle timelineStyle, TimelineParams timelineParams )
        {
            _traces = traces;
            _timelineStyle = timelineStyle;
            _timelineParams = timelineParams;
        }

        Bookmark FindBookmarkByName( string name )
        {
            foreach ( var b in _bookmarks )
            {
                if ( b.Name == name )
                    return b;
            }

            return null;
        }

        #region Event handlers

        public void OnLoaded( object sender, RoutedEventArgs e )
        {
            Focus();
        }

        public void OnSizeChanged( object sender, SizeChangedEventArgs e )
        {
            if ( e.PreviousSize.Width > 0.0 && (e.WidthChanged || e.HeightChanged) )
            {
                e.Handled = true;
                _timelineParams.PixelsPerSecond = _timelineParams.PixelsPerSecond * e.NewSize.Width / e.PreviousSize.Width;

                Draw();
            }
        }

        public void OnKeyDown( object sender, KeyEventArgs e )
        {
            bool handled = true;

            // TODO custom bindings
            if ( e.Key == Key.A && Keyboard.IsKeyDown( Key.LeftCtrl ) ) // select all
            {
                foreach ( var trace in _traces )
                    if ( trace.Visible )
                        SelectTraceRecursive( trace, SelectionUpdateMode.Set );
                Draw();
                ProcesSelection();
            }
            else if ( e.Key >= Key.D0 && e.Key <= Key.D9 ) // fine goto
            {
                int num = e.Key - Key.D0;
                double newPos = _timelineParams.TimeRangeStart +
                    num / 9.0 * (_timelineParams.TimeRangeEnd - _timelineParams.TimeRangeStart);
                GoToTime( newPos );
            }
            else if ( e.Key == Key.G ) // goto
            {
                QADialog inputDialog = new QADialog( "Type location", "0.0" );
                if ( inputDialog.Show() )
                {
                    Bookmark bookmark = FindBookmarkByName( inputDialog.Answer );
                    if ( bookmark != null )
                        GoToTime( bookmark.Time );
                    else
                    {
                        double newTime = double.Parse( inputDialog.Answer );
                        GoToTime( newTime );
                    }
                }
            }
            else if ( e.Key == Key.B )
            {
                QADialog inputDialog = new QADialog( "Bookmark name:", "unnamed" );
                if ( inputDialog.Show() )
                {
                    _bookmarks.Add( new Bookmark( inputDialog.Answer, _timelineParams.CursorPos ) );
                    Draw();
                }
            }
            else if ( e.Key == Key.F && Keyboard.IsKeyDown( Key.LeftCtrl ) ) // find events
            {
                QADialog inputDialog = new QADialog( "Search:", "" );
                if ( inputDialog.Show() && inputDialog.Answer.Length > 0 )
                {
                    foreach ( var trace in _traces )
                    {
                        if ( !trace.Visible )
                            continue;
                        trace.SelectEventsContainingString( inputDialog.Answer, _selection );
                    }
                    Draw();
                    ProcesSelection();
                }
            }
            else if ( e.Key == Key.Left ) // move cursor left
            {
                double offset = _timelineParams.SecondsPerPixel * ActualWidth * 0.1;
                _timelineParams.TimeOffset -= offset;
                _timelineParams.CursorPos -= offset;
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Right ) // move cursor right
            {
                double offset = _timelineParams.SecondsPerPixel * ActualWidth * 0.1;
                _timelineParams.TimeOffset += offset;
                _timelineParams.CursorPos += offset;
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Add )
            {
                _timelineParams.ZoomAtTime( 1, _timelineParams.CursorPos );
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Subtract )
            {
                _timelineParams.ZoomAtTime( -1, _timelineParams.CursorPos );
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Up )
            {
                _timelineParams.YOffset += 50.0;
                if ( _timelineParams.YOffset > 0 )
                    _timelineParams.YOffset = 0;
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Down )
            {
                _timelineParams.YOffset -= 50.0;
                _timelineParams.OnDrawDelegate();
            }
            else if ( e.Key == Key.Escape ) // abort mouse interaction
            {
                _mouseDragMode = MouseDragMode.None;
                Draw();
            }
            else
                handled = false;

            e.Handled = handled;
        }

        public void OnMouseDown( object sender, MouseButtonEventArgs e )
        {
            e.Handled = true;
            Focus();

            bool capture = false;
            Point p = e.GetPosition( this );

            if ( e.ChangedButton == MouseButton.Left ) // selection
            {
                if ( e.ClickCount == 1 ) // start simple select
                {
                    _timeSelection = (p.Y > ActualHeight - _timelineStyle.TimeBarHeight);
                    _mouseDragStart = p;
                    _mouseDragStartTimeOffset = _timelineParams.TimeOffset;
                    _mouseDragMode = MouseDragMode.Select;

                    if ( _timeSelection )
                    {
                        _selectionRectStart = new Point( p.X, 10000.0 );
                        _selectionRectEnd = new Point( p.X, 0 );
                    }
                    else
                    {
                        _selectionRectStart = p;
                        _selectionRectEnd = p;
                    }

                    Draw();
                }
                else if ( e.ClickCount == 2 )
                {
                    ProcessDoubleClick( _mouseLastHoverPos );
                    _mouseDragMode = MouseDragMode.None;
                    Draw();
                }
            }
            else if ( e.ChangedButton == MouseButton.Right ) // moving
            {
                _mouseDragStart = p;
                _mouseDragStartTimeOffset = _timelineParams.TimeOffset;
                _mouseDragStartYOffset = _timelineParams.YOffset;
                _mouseDragMode = MouseDragMode.Move;
                capture = true;
            }

            if ( capture )
            {
                _mouseLastHoverPos.X = -1;
                _mouseLastHoverPos.Y = -1;
            }

            CaptureMouse();
        }

        public void OnMouseMove( object sender, MouseEventArgs e )
        {
            Point p = e.GetPosition( this );

            if ( _mouseDragMode == MouseDragMode.None )
            {
                if ( _mouseLastHoverPos.X != p.X || _mouseLastHoverPos.Y != p.Y )
                {
                    _mouseLastHoverPos = p;
                    Draw();
                }
            }
            else if ( _mouseDragMode == MouseDragMode.Select )
            {
                if ( _timeSelection )
                    _selectionRectEnd = new Point( p.X, 0 );
                else
                    _selectionRectEnd = p;

                Draw();
            }
            else if ( _mouseDragMode == MouseDragMode.Move )
            {
                double deltaX = p.X - _mouseDragStart.X;
                _timelineParams.TimeOffset = _mouseDragStartTimeOffset - deltaX * _timelineParams.SecondsPerPixel;

                double deltaY = _mouseDragStart.Y - p.Y;
                _timelineParams.YOffset = _mouseDragStartYOffset - deltaY;
                if ( _timelineParams.YOffset > 0 )
                    _timelineParams.YOffset = 0;

                _timelineParams.OnDrawDelegate();
            }
        }

        public void OnMouseUp( object sender, MouseButtonEventArgs e )
        {
            Point p = e.GetPosition( this );

            if ( e.ChangedButton == MouseButton.Left )
            {
                if ( _mouseDragMode == MouseDragMode.Select )
                {
                    _mouseDragMode = MouseDragMode.None;

                    SelectionUpdateMode selectionMode = SelectionUpdateMode.Set;
                    if ( Keyboard.IsKeyDown( Key.LeftCtrl ) )
                        selectionMode = SelectionUpdateMode.Invert;
                    else if ( Keyboard.IsKeyDown( Key.LeftShift ) )
                        selectionMode = SelectionUpdateMode.Add;

                    UpdateSelection( selectionMode );
                    Draw();
                }
            }
            else if ( e.ChangedButton == MouseButton.Right )
            {
                if ( p.Equals( _mouseDragStart ) )
                    _timelineParams.CursorPos = _timelineParams.CanvasXToTime( p.X );

                if ( _mouseDragMode == MouseDragMode.Move )
                    _mouseDragMode = MouseDragMode.None;

                _timelineParams.OnDrawDelegate();
            }

            ReleaseMouseCapture();
        }

        public void OnFocusLost( object sender, RoutedEventArgs e )
        {
            if ( _mouseDragMode != MouseDragMode.None )
            {
                _mouseDragMode = MouseDragMode.None;
                ReleaseMouseCapture();
                Draw();
            }
        }

        public void OnMouseWheel( object sender, MouseWheelEventArgs e )
        {
            Point p = e.GetPosition( this );

            double selectionStartBefore = _timelineParams.CanvasXToTime( _selectionRectStart.X );

            _timelineParams.ZoomAtCanvasX( e.Delta, p.X );

            if ( _mouseDragMode != MouseDragMode.None )
            {
                _mouseDragStart = p;
                _mouseDragStartTimeOffset = _timelineParams.TimeOffset;
                _mouseDragStartYOffset = _timelineParams.YOffset;
            }

            // correct selection rect start after zooming
            if ( _mouseDragMode == MouseDragMode.Select )
                _selectionRectStart.X = _timelineParams.TimeToCanvasX( selectionStartBefore );

            _timelineParams.OnDrawDelegate();
        }

        #endregion Input handlers

        #region Drawing

        public void Draw()
        {
            // Retrieve the DrawingContext in order to create new drawing content.
            DrawingContext ctx = drawingVisual.RenderOpen();

            // main clip
            double width = ActualWidth;
            double height = ActualHeight;
            ctx.PushClip( new RectangleGeometry( new Rect( 0, 0, width, height ) ) );

            // clear
            {
                Rect rect = new Rect( new Point( 0, 0 ), new Size( ActualWidth, ActualHeight ) );
                ctx.DrawRectangle( _timelineStyle.BackgroundBrush, null, rect );
            }

            TimeBar.DrawBackground( ctx, width, height, _timelineParams, _timelineStyle );

            int y = (int)_timelineParams.YOffset;

            foreach ( var trace in _traces )
                DrawTrace( ctx, trace, ref y );

            // draw selection
            if ( _mouseDragMode == MouseDragMode.Select &&
                (_selectionRectStart.Y != _selectionRectEnd.Y || _selectionRectStart.X != _selectionRectEnd.X) )
            {
                Rect rect = new Rect( _selectionRectStart, _selectionRectEnd );
                rect.Inflate( 0.5, 0.5 );
                ctx.DrawRectangle( _timelineStyle.SelectionRectBrush, _timelineStyle.SelectionRectPen, rect );

                // draw text
                double timeStart = _timelineParams.CanvasXToTime( _selectionRectStart.X );
                double timeEnd = _timelineParams.CanvasXToTime( _selectionRectEnd.X );
                double duration = Math.Abs( timeEnd - timeStart );

                double X = Math.Min( _selectionRectStart.X, _selectionRectEnd.X );
                double Y = Math.Max( _selectionRectStart.Y, _selectionRectEnd.Y );
                FormattedText text = new FormattedText( Logic.Utils.FormatValue( duration, Logic.ValueType.Time ), _timelineStyle.CultureInfo,
                    FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 12.0, _timelineStyle.SelectionRectPen.Brush );
                ctx.DrawText( text, new Point( X + 4, Y + 4 ) );
            }

            TimeBar.DrawOverlay( ctx, width, height, _timelineParams, _timelineStyle );

            DrawBookmarks( ctx );

            // draw cursor
            {
                double cursorX = _timelineParams.TimeToCanvasX( _timelineParams.CursorPos );
                ctx.DrawLine( _timelineStyle.CursorPen, new Point( cursorX, 0.0 ), new Point( cursorX, ActualHeight ) );
            }

            ctx.Pop();
            ctx.Close();
        }

        void DrawEvent( DrawingContext ctx, Event evt, int y )
        {
            double realWidth = _timelineParams.PixelsPerSecond * evt.GetLength();

            Point leftTop = new Point( _timelineParams.TimeToCanvasX( evt.Evt.StartTime ), y );
            Rect rect = new Rect( leftTop, new Size( realWidth, _timelineStyle.BlockHeight ) );

            Brush brush = null;
            if ( evt.Style == Event.DrawStyle.Highlighted )
            {
                brush = _timelineStyle.HighlightedBlockBrush;
                rect.Inflate( 0.0, 1.0 );
            }
            else if ( evt.Style == Event.DrawStyle.Selected )
            {
                brush = _timelineStyle.SelectedBlockBrush;
                rect.Inflate( 0.0, 1.0 );
            }
            else
            {
                // TODO this can be slow
                brush = new SolidColorBrush( evt.Color );
            }

            _drawnEvents.Add( evt, rect );
            ctx.DrawRectangle( brush, null, rect );

            // draw caption
            if ( realWidth > 15 )
            {
                const int textMargin = 3;
                Point textPos = new Point( _timelineParams.TimeToCanvasX( evt.Evt.StartTime ) + textMargin, y + textMargin );
                Rect textClipRect = new Rect( leftTop, new Size( realWidth - 2 * textMargin, _timelineStyle.BlockHeight ) );

                ctx.PushClip( new RectangleGeometry( textClipRect ) );
                {
                    FormattedText text = new FormattedText( evt.GetCaption(), _timelineStyle.CultureInfo,
                        FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 10.0, _timelineStyle.GroupNameBrush );
                    ctx.DrawText( text, textPos );
                }
                ctx.Pop();
            }
        }

        void DrawEventNode( DrawingContext ctx, EventNode node, int y )
        {
            double end = _timelineParams.TimeToCanvasX( node.End );

            if ( end < 0.0 )
                return;

            double start = _timelineParams.TimeToCanvasX( node.Start );
            if ( start > ActualWidth )
                return;

            // merge blocks if not a leaf node and apperant width is too small
            if ( node.TotalEvents > 1 &&
                _timelineParams.PixelsPerSecond * (node.End - node.Start) < _timelineStyle.BlockMergeTreshold )
            {
                Rect rect = new Rect( new Point( start, y ), new Point( end, y + _timelineStyle.BlockHeight ) );
                Brush brush = null;

                if ( node.Style == Event.DrawStyle.Highlighted )
                {
                    brush = _timelineStyle.HighlightedBlockBrush;
                    rect.Inflate( 0.0, 1.0 );
                }
                else if ( node.Style == Event.DrawStyle.Selected )
                {
                    brush = _timelineStyle.SelectedBlockBrush;
                    rect.Inflate( 0.0, 1.0 );
                }
                else
                {
                    // make the merged block better visible:
                    double weight = Math.Sqrt( node.weight );
                    rect.Inflate( 0.05, 0.0 );

                    Color color = node.BlockColor;
                    brush = new SolidColorBrush( Color.FromArgb( (byte)(255.0 * weight), color.R, color.G, color.B ) );
                }

                // draw merged
                ctx.DrawRectangle( brush, null, rect );
                return;
            }

            if ( node.Evt != null )
            {
                DrawEvent( ctx, node.Evt, y );
            }
            else
            {
                DrawEventNode( ctx, node.Left, y );
                DrawEventNode( ctx, node.Right, y );
            }
        }

        void DrawGroup( DrawingContext ctx, MergedGroup group, ref int y )
        {
            ctx.DrawLine( _timelineStyle.GroupLinePen, new Point( 0.0, y + 0.5 ), new Point( ActualWidth, y + 0.5 ) );

            FormattedText text = new FormattedText( group.Name, _timelineStyle.CultureInfo,
                FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 11.0, _timelineStyle.GroupNameBrush );
            ctx.DrawText( text, new Point( 4, y + 4 ) );

            y += _timelineStyle.GroupTitleSpacing;

            // draw group levels
            foreach ( var pair in group.Levels )
            {
                GroupLevel level = pair.Value;

                if ( y > -_timelineStyle.BlockHeight && y <= ActualHeight )
                    if ( level.RootNode != null )
                        DrawEventNode( ctx, level.RootNode, y );

                y += _timelineStyle.BlockHeight + _timelineStyle.LevelSpacing;
            }

            y += _timelineStyle.PostGroupSpacing;
        }

        void DrawTrace( DrawingContext ctx, ProfilerTraceInstance trace, ref int y )
        {
            if ( !trace.Visible )
                return;

            ctx.DrawLine( _timelineStyle.ViewLinePen, new Point( 0.0, y + 0.5 ), new Point( ActualWidth, y + 0.5 ) );

            FormattedText text = new FormattedText( trace.Name, _timelineStyle.CultureInfo,
                FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 12.0, _timelineStyle.GroupNameBrush );
            ctx.DrawText( text, new Point( 4, y + 4 ) );

            y += _timelineStyle.ViewTitleSpacing;

            foreach ( var view in trace.DataViews )
            {
                if ( !view.Visible )
                    continue;

                // draw regions
                foreach ( var pair in view.Regions )
                {
                    Region region = pair.Value;
                    double x0 = _timelineParams.TimeToCanvasX( region.TimeStart );
                    double x1 = _timelineParams.TimeToCanvasX( region.TimeEnd );

                    if ( x1 < 0 || x0 > ActualWidth || x0 > x1 )
                        continue;

                    Rect rect = new Rect( new Point( x0, 0 ), new Size( x1 - x0, ActualHeight ) );
                    ctx.DrawRectangle( _timelineStyle.RegionBrush, null, rect );

                    if ( x1 - x0 > 10 )
                    {
                        ctx.PushClip( new RectangleGeometry( rect ) );
                        {
                            FormattedText regionText = new FormattedText( region.Name, _timelineStyle.CultureInfo,
                                FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 11.0, _timelineStyle.GroupNameBrush );
                            ctx.DrawText( regionText, new Point( x0 + 2, 2 ) );
                        }
                        ctx.Pop();
                    }
                }
            }

            _drawnEvents.Clear();

            foreach ( var g in trace.Groups )
            {
                DrawGroup( ctx, g.Value, ref y );
            }

            // draw links (dependencies)
            // TODO: links between invisible events (beyond viewport) are not shown
            // TODO: links between merged event blocks are not shown
            foreach ( var drawnEvent in _drawnEvents )
            {
                Event endEvent = drawnEvent.Key;
                Rect endEventRect = drawnEvent.Value;

                if ( endEvent.DependencyStart == null )
                    continue;

                foreach ( var startEvent in drawnEvent.Key.DependencyStart )
                {
                    if ( !_drawnEvents.ContainsKey( startEvent ) )
                        continue;

                    Rect startEventRect = _drawnEvents[startEvent];
                    Point startPoint = new Point( startEventRect.Right, (startEventRect.Top + startEventRect.Bottom) / 2.0 );
                    Point endPoint = new Point( endEventRect.Left, (endEventRect.Top + endEventRect.Bottom) / 2.0 );

                    double alpha = 0.7;
                    double slope = endPoint.X - startPoint.X;
                    const double slopeTreshold = 2.0;
                    if ( slope < slopeTreshold )
                        alpha *= slope / slopeTreshold;

                    Brush linkBrush = new SolidColorBrush( Color.FromArgb( (byte)(255.0 * alpha), 50, 255, 50 ) );
                    Pen linkPen = new Pen( linkBrush, 1 );

                    BezierSegment segment = new BezierSegment(
                        new Point( startPoint.X + 10, startPoint.Y ),
                        new Point( endPoint.X - 10, endPoint.Y ),
                        endPoint, true );
                    PathFigure figure = new PathFigure( startPoint, new PathSegment[1] { segment }, false );
                    figure.IsFilled = false;
                    Geometry path = new PathGeometry( new PathFigure[1] { figure } );
                    ctx.DrawGeometry( linkBrush, linkPen, path );
                }
            }
        }

        void DrawBookmarks( DrawingContext ctx )
        {
            foreach ( var b in _bookmarks )
            {
                double x = _timelineParams.TimeToCanvasX( b.Time );

                FormattedText text = new FormattedText( b.Name, _timelineStyle.CultureInfo,
                    FlowDirection.LeftToRight, _timelineStyle.TextTypeface, 10.0, b.Brush );
                ctx.DrawText( text, new Point( x + 2, ActualHeight - 14.0 ) );

                ctx.DrawLine( b.Pen, new Point( x, 0.0 ), new Point( x, ActualHeight ) );
            }
        }

        #endregion Drawing

        #region Selecting

        private void UpdateSelection( SelectionUpdateMode selectionMode )
        {
            Rect selectionRect = new Rect(
                Math.Min( _selectionRectStart.X, _selectionRectEnd.X ),
                Math.Min( _selectionRectStart.Y, _selectionRectEnd.Y ),
                Math.Abs( _selectionRectStart.X - _selectionRectEnd.X ),
                Math.Abs( _selectionRectStart.Y - _selectionRectEnd.Y ) );

            int y = (int)_timelineParams.YOffset;

            foreach ( var trace in _traces )
            {
                if ( !trace.Visible )
                    continue;

                if ( selectionMode == SelectionUpdateMode.Set )
                {
                    SelectTraceRecursive( trace, SelectionUpdateMode.Unset );
                    SelectTrace( selectionRect, trace, ref y, SelectionUpdateMode.Add );
                }
                else if ( selectionMode == SelectionUpdateMode.Add )
                    SelectTrace( selectionRect, trace, ref y, SelectionUpdateMode.Add );
                else if ( selectionMode == SelectionUpdateMode.Invert )
                    SelectTrace( selectionRect, trace, ref y, SelectionUpdateMode.Invert );
            }

            ProcesSelection();
        }

        Event.DrawStyle SelectEvent( Event evt, SelectionUpdateMode mode )
        {
            switch ( mode )
            {
                case SelectionUpdateMode.Add:
                case SelectionUpdateMode.Set:
                    _selection.Add( evt );
                    evt.Style = Event.DrawStyle.Selected;
                    break;
                case SelectionUpdateMode.Unset:
                    _selection.Remove( evt );
                    evt.Style = Event.DrawStyle.Normal;
                    break;
                case SelectionUpdateMode.Invert:
                    if ( evt.Style != Event.DrawStyle.Normal )
                    {
                        _selection.Remove( evt );
                        evt.Style = Event.DrawStyle.Normal;
                    }
                    else
                    {
                        _selection.Add( evt );
                        evt.Style = Event.DrawStyle.Selected;
                    }
                    break;
            }

            return evt.Style;
        }

        Event.DrawStyle SelectEvent( Rect selectionRect, Event evt, int y, SelectionUpdateMode mode )
        {
            double realWidth = _timelineParams.PixelsPerSecond * evt.GetLength();

            Point leftTop = new Point( _timelineParams.TimeToCanvasX( evt.Evt.StartTime ), y );
            Rect rect = new Rect( leftTop, new Size( realWidth, _timelineStyle.BlockHeight ) );

            if ( selectionRect.IntersectsWith( rect ) )
                SelectEvent( evt, mode );

            return evt.Style;
        }

        // update selection for overlapping selection rect
        Event.DrawStyle SelectEventNode( Rect selectionRect, EventNode node, int y, SelectionUpdateMode mode )
        {
            double end = _timelineParams.TimeToCanvasX( node.End );
            if ( end < selectionRect.Left )
                return node.Style;

            double start = _timelineParams.TimeToCanvasX( node.Start );
            if ( start > selectionRect.Right )
                return node.Style;

            Rect rect = new Rect( new Point( start, y ), new Point( end, y + _timelineStyle.BlockHeight ) );

            if ( selectionRect.Contains( rect ) )
                SelectEventNodeRecursive( node, mode );
            else if ( selectionRect.IntersectsWith( rect ) )
            {
                if ( node.Evt != null )
                    node.Style = SelectEvent( selectionRect, node.Evt, y, mode );
                else
                {
                    node.Style = Event.CombineDrawStyles(
                        SelectEventNode( selectionRect, node.Left, y, mode ),
                        SelectEventNode( selectionRect, node.Right, y, mode ) );
                }
            }

            return node.Style;
        }

        void SelectGroup( Rect selectionRect, MergedGroup group, ref int y, SelectionUpdateMode mode )
        {
            y += _timelineStyle.GroupTitleSpacing;

            foreach ( var pair in group.Levels )
            {
                GroupLevel level = pair.Value;

                if ( y + _timelineStyle.BlockHeight > selectionRect.Top && y <= selectionRect.Bottom )
                    if ( level.RootNode != null )
                        SelectEventNode( selectionRect, level.RootNode, y, mode );

                y += _timelineStyle.BlockHeight + _timelineStyle.LevelSpacing;
            }

            y += _timelineStyle.PostGroupSpacing;
        }

        void SelectTrace( Rect selectionRect, ProfilerTraceInstance trace, ref int y, SelectionUpdateMode mode )
        {
            y += _timelineStyle.ViewTitleSpacing;

            foreach ( var g in trace.Groups )
            {
                SelectGroup( selectionRect, g.Value, ref y, mode );
            }
        }

        Event GetHoveredEvent( EventNode node, int y, Point p )
        {
            double end = _timelineParams.TimeToCanvasX( node.End );
            double start = _timelineParams.TimeToCanvasX( node.Start );

            Rect rect = new Rect( new Point( start, y ), new Point( end, y + _timelineStyle.BlockHeight ) );

            if ( rect.Contains( p ) )
            {
                if ( node.Evt != null )
                    return node.Evt;
                else
                {
                    Event e = GetHoveredEvent( node.Left, y, p );
                    if ( e != null )
                        return e;
                    e = GetHoveredEvent( node.Right, y, p );
                    if ( e != null )
                        return e;
                }
            }

            return null;
        }

        void ProcessDoubleClick( Point p )
        {
            int y = (int)_timelineParams.YOffset;

            foreach ( var trace in _traces )
            {
                y += _timelineStyle.ViewTitleSpacing;
                foreach ( var groupPair in trace.Groups )
                {
                    MergedGroup group = groupPair.Value;

                    y += _timelineStyle.GroupTitleSpacing;
                    foreach ( var pair in group.Levels )
                    {
                        GroupLevel level = pair.Value;

                        if ( y + _timelineStyle.BlockHeight > p.Y && y <= p.Y )
                        {
                            if ( level.RootNode != null )
                            {
                                Event e = GetHoveredEvent( level.RootNode, y, p );
                                if ( e != null )
                                {
                                    var events = group.SelectSubevents( e );
                                    _selection.UnionWith( events );
                                    ProcesSelection();
                                    return;
                                }
                            }
                        }

                        y += _timelineStyle.BlockHeight + _timelineStyle.LevelSpacing;
                    }

                    y += _timelineStyle.PostGroupSpacing;
                }
            }

            ProcesSelection();
        }

        // TODO move to ProfilerTraceInstance
        Event.DrawStyle SelectEventNodeRecursive( EventNode node, SelectionUpdateMode mode )
        {
            if ( node.Evt != null )
                node.Style = SelectEvent( node.Evt, mode );
            else
                node.Style = Event.CombineDrawStyles(
                    SelectEventNodeRecursive( node.Left, mode ), SelectEventNodeRecursive( node.Right, mode ) );

            return node.Style;
        }

        // TODO move to ProfilerTraceInstance
        void SelectTraceRecursive( ProfilerTraceInstance trace, SelectionUpdateMode mode )
        {
            foreach ( var group in trace.Groups )
            {
                foreach ( var pair in group.Value.Levels )
                {
                    GroupLevel level = pair.Value;

                    if ( level.RootNode != null )
                        SelectEventNodeRecursive( level.RootNode, mode );
                }
            }
        }

        void ProcesSelection()
        {
            if ( _onShowSelectionDetails == null )
                return;

            var eventsPerView = new Dictionary<ProfilerDataView, List<Event>>();

            foreach ( var e in _selection )
            {
                if ( !eventsPerView.ContainsKey( e.ParentView ) )
                    eventsPerView.Add( e.ParentView, new List<Event>() );

                eventsPerView[e.ParentView].Add( e );
            }

            string details = "";
            foreach ( var p in eventsPerView )
            {
                details += p.Key.Name.ToUpper() + ":\n";
                details += p.Key.GetDescription( p.Value );
                details += "\n\n";
            }

            // TODO
            foreach ( var trace in _traces )
            {
                foreach ( var group in trace.Groups )
                {
                    foreach ( var pair in group.Value.Levels )
                    {
                        GroupLevel level = pair.Value;
                        level.UpdateSelection();
                    }
                }
            }

            _onShowSelectionDetails( details );
        }

        #endregion Selecting

        public void GoToTime( double time )
        {
            _timelineParams.CursorPos = time;
            _timelineParams.TimeOffset = time - _timelineParams.SecondsPerPixel * ActualWidth / 2.0;

            Draw();
        }
    }
}
